/*******************************************************************************
 * Copyright (c) 2000, 2015 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.ui;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import org.eclipse.core.runtime.Adapters;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.ui.actions.SimpleWildcardTester;
import org.eclipse.ui.internal.ActionExpression;
import org.eclipse.ui.internal.registry.IWorkbenchRegistryConstants;
import org.eclipse.ui.model.IWorkbenchAdapter;
import org.osgi.framework.Bundle;

/**
 * Determines the enablement status given a selection. This calculation is done
 * based on the definition of the <code>enablesFor</code> attribute,
 * <code>enablement</code> element, and the <code>selection</code> element found
 * in the <code>IConfigurationElement</code> provided.
 * <p>
 * This class can be instantiated by clients. It is not intended to be extended.
 * </p>
 *
 * @since 3.0
 *
 *        Note: The dependency on org.eclipse.jface.text for ITextSelection must
 *        be severed It may be possible to do with IActionFilter generic
 *        workbench registers IActionFilter for "size" property against
 *        IStructuredSelection workbench text registers IActionFilter for "size"
 *        property against ITextSelection code here:
 *        sel.getAdapter(IActionFilter.class) As an interim solution, use
 *        reflection to access selections implementing ITextSelection
 */
public final class SelectionEnabler {

	/* package */static class SelectionClass {
		public String className;

		public String nameFilter;

		public boolean recursive;
	}

	/**
	 * Enablement mode value for ANY_NUMBER
	 */
	public static final int ANY_NUMBER = -2;

	/**
	 * The constant integer hash code value meaning the hash code has not yet been
	 * computed.
	 */
	private static final int HASH_CODE_NOT_COMPUTED = -1;

	/**
	 * A factor for computing the hash code for all schemes.
	 */
	private static final int HASH_FACTOR = 89;

	/**
	 * The seed for the hash code for all schemes.
	 */
	private static final int HASH_INITIAL = SelectionEnabler.class.getName().hashCode();

	/**
	 * Cached value of <code>org.eclipse.jface.text.ITextSelection.class</code>;
	 * <code>null</code> if not initialized or not present.
	 */
	private static Class<?> iTextSelectionClass = null;

	/**
	 * Hard-wired id of the JFace text plug-in (not on pre-req chain).
	 */
	private static final String JFACE_TEXT_PLUG_IN = "org.eclipse.jface.text"; //$NON-NLS-1$

	/**
	 * Enablement mode value for MULTIPLE
	 */
	public static final int MULTIPLE = -5;

	/**
	 * Enablement mode value for NONE
	 */
	public static final int NONE = -4;

	/**
	 * Enablement mode value for NONE_OR_ONE
	 */
	public static final int NONE_OR_ONE = -3;

	/**
	 * Enablement mode value for ONE_OR_MORE
	 */
	public static final int ONE_OR_MORE = -1;

	/**
	 * Hard-wired fully qualified name of the text selection class (not on pre-req
	 * chain).
	 */
	private static final String TEXT_SELECTION_CLASS = "org.eclipse.jface.text.ITextSelection"; //$NON-NLS-1$

	/**
	 * Indicates whether the JFace text plug-in is even around. Without the JFace
	 * text plug-in, text selections are moot.
	 */
	private static boolean textSelectionPossible = true;

	/**
	 * Enablement mode value for UNKNOWN
	 */
	public static final int UNKNOWN = 0;

	/**
	 * Returns <code>ITextSelection.class</code> or <code>null</code> if the class
	 * is not available.
	 *
	 * @return <code>ITextSelection.class</code> or <code>null</code> if class not
	 *         available
	 * @since 3.0
	 */
	private static Class<?> getTextSelectionClass() {
		if (iTextSelectionClass != null) {
			// tried before and succeeded
			return iTextSelectionClass;
		}
		if (!textSelectionPossible) {
			// tried before and failed
			return null;
		}

		// JFace text plug-in is not on prereq chain of generic wb plug-in
		// hence: ITextSelection.class won't compile
		// and Class.forName("org.eclipse.jface.text.ITextSelection") won't find
		// it need to be trickier...
		Bundle bundle = Platform.getBundle(JFACE_TEXT_PLUG_IN);
		if (bundle == null || bundle.getState() == Bundle.UNINSTALLED) {
			// JFace text plug-in is not around, or has already
			// been removed, assume that it will never be around
			textSelectionPossible = false;
			return null;
		}

		// plug-in is around
		// it's not our job to activate the plug-in
		if (bundle.getState() == Bundle.INSTALLED) {
			// assume it might come alive later
			textSelectionPossible = true;
			return null;
		}

		try {
			Class<?> c = bundle.loadClass(TEXT_SELECTION_CLASS);
			// remember for next time
			iTextSelectionClass = c;
			return iTextSelectionClass;
		} catch (ClassNotFoundException e) {
			// unable to load ITextSelection - sounds pretty serious
			// treat as if JFace text plug-in were unavailable
			textSelectionPossible = false;
			return null;
		}
	}

	/**
	 * Verifies that the given name matches the given wildcard filter. Returns true
	 * if it does.
	 *
	 * @param name   the name to match
	 * @param filter the filter to match to
	 * @return <code>true</code> if there is a match
	 */
	public static boolean verifyNameMatch(String name, String filter) {
		return SimpleWildcardTester.testWildcardIgnoreCase(filter, name);
	}

	private List<SelectionClass> classes = new ArrayList<>();

	private ActionExpression enablementExpression;

	/**
	 * The hash code for this object. This value is computed lazily, and marked as
	 * invalid when one of the values on which it is based changes.
	 */
	private transient int hashCode = HASH_CODE_NOT_COMPUTED;

	private int mode = UNKNOWN;

	/**
	 * Create a new instance of the receiver.
	 *
	 * @param configElement the configuration element to parse
	 */
	public SelectionEnabler(IConfigurationElement configElement) {
		super();
		if (configElement == null) {
			throw new IllegalArgumentException();
		}
		parseClasses(configElement);
	}

	@Override
	public boolean equals(final Object object) {
		if (object instanceof SelectionEnabler) {
			final SelectionEnabler that = (SelectionEnabler) object;
			return Objects.equals(this.classes, that.classes)
					&& Objects.equals(this.enablementExpression, that.enablementExpression)
					&& this.mode == that.mode;
		}

		return false;
	}

	/**
	 * Computes the hash code for this object based on the id.
	 *
	 * @return The hash code for this object.
	 */
	@Override
	public int hashCode() {
		if (hashCode == HASH_CODE_NOT_COMPUTED) {
			hashCode = HASH_INITIAL * HASH_FACTOR + Objects.hashCode(classes);
			hashCode = hashCode * HASH_FACTOR + Objects.hashCode(enablementExpression);
			hashCode = hashCode * HASH_FACTOR + Integer.hashCode(mode);
			if (hashCode == HASH_CODE_NOT_COMPUTED) {
				hashCode++;
			}
		}
		return hashCode;
	}

	/**
	 * Returns true if given structured selection matches the conditions specified
	 * in the registry for this action.
	 */
	private boolean isEnabledFor(ISelection sel) {
		Object obj = sel;
		int count = sel.isEmpty() ? 0 : 1;

		if (verifySelectionCount(count) == false) {
			return false;
		}

		// Compare selection to enablement expression.
		if (enablementExpression != null) {
			return enablementExpression.isEnabledFor(obj);
		}

		// Compare selection to class requirements.
		if (classes.isEmpty()) {
			return true;
		}
		if (obj instanceof IAdaptable) {
			IAdaptable element = (IAdaptable) obj;
			if (verifyElement(element) == false) {
				return false;
			}
		} else {
			return false;
		}

		return true;
	}

	/**
	 * Returns true if given text selection matches the conditions specified in the
	 * registry for this action.
	 */
	private boolean isEnabledFor(ISelection sel, int count) {
		if (verifySelectionCount(count) == false) {
			return false;
		}

		// Compare selection to enablement expression.
		if (enablementExpression != null) {
			return enablementExpression.isEnabledFor(sel);
		}

		// Compare selection to class requirements.
		if (classes.isEmpty()) {
			return true;
		}
		for (SelectionClass sc : classes) {
			if (verifyClass(sel, sc.className)) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Returns true if given structured selection matches the conditions specified
	 * in the registry for this action.
	 */
	private boolean isEnabledFor(IStructuredSelection ssel) {
		int count = ssel.size();

		if (verifySelectionCount(count) == false) {
			return false;
		}

		// Compare selection to enablement expression.
		if (enablementExpression != null) {
			return enablementExpression.isEnabledFor(ssel);
		}

		// Compare selection to class requirements.
		if (classes.isEmpty()) {
			return true;
		}
		for (Object obj : ssel) {
			if (obj instanceof IAdaptable) {
				IAdaptable element = (IAdaptable) obj;
				if (verifyElement(element) == false) {
					return false;
				}
			} else {
				return false;
			}
		}

		return true;
	}

	/**
	 * Check if the receiver is enabled for the given selection.
	 *
	 * @param selection the selection
	 * @return <code>true</code> if the given selection matches the conditions
	 *         specified in <code>IConfirgurationElement</code>.
	 */
	public boolean isEnabledForSelection(ISelection selection) {
		// Optimize it.
		if (mode == UNKNOWN) {
			return false;
		}

		// Handle undefined selections.
		if (selection == null) {
			selection = StructuredSelection.EMPTY;
		}

		// According to the dictionary, a selection is "one that
		// is selected", or "a collection of selected things".
		// In reflection of this, we deal with one or a collection.

		// special case: structured selections
		if (selection instanceof IStructuredSelection) {
			return isEnabledFor((IStructuredSelection) selection);
		}

		// special case: text selections
		// Code should read
		// if (selection instanceof ITextSelection) {
		// int count = ((ITextSelection) selection).getLength();
		// return isEnabledFor(selection, count);
		// }
		// use Java reflection to avoid dependence of org.eclipse.jface.text
		// which is in an optional part of the generic workbench
		Class<?> tselClass = getTextSelectionClass();
		if (tselClass != null && tselClass.isInstance(selection)) {
			try {
				Method m = tselClass.getDeclaredMethod("getLength"); //$NON-NLS-1$
				Object r = m.invoke(selection);
				if (r instanceof Integer) {
					return isEnabledFor(selection, ((Integer) r).intValue());
				}
				// should not happen - but enable if it does
				return true;
			} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
				// should not happen - fall through if it does
			}
		}

		// all other cases
		return isEnabledFor(selection);
	}

	/**
	 * Parses registry element to extract mode and selection elements that will be
	 * used for verification.
	 */
	private void parseClasses(IConfigurationElement config) {
		// Get enables for.
		String enablesFor = config.getAttribute(IWorkbenchRegistryConstants.ATT_ENABLES_FOR);
		if (enablesFor == null) {
			enablesFor = "*"; //$NON-NLS-1$
		}
		switch (enablesFor) {
		case "*": //$NON-NLS-1$
			mode = ANY_NUMBER;
			break;
		case "?": //$NON-NLS-1$
			mode = NONE_OR_ONE;
			break;
		case "!": //$NON-NLS-1$
			mode = NONE;
			break;
		case "+": //$NON-NLS-1$
			mode = ONE_OR_MORE;
			break;
		case "multiple": //$NON-NLS-1$
		case "2+": //$NON-NLS-1$
			mode = MULTIPLE;
			break;
		default:
			try {
				mode = Integer.parseInt(enablesFor);
			} catch (NumberFormatException e) {
				mode = UNKNOWN;
			}
			break;
		}

		// Get enablement block.
		IConfigurationElement[] children = config.getChildren(IWorkbenchRegistryConstants.TAG_ENABLEMENT);
		if (children.length > 0) {
			enablementExpression = new ActionExpression(children[0]);
			return;
		}

		// Get selection block.
		children = config.getChildren(IWorkbenchRegistryConstants.TAG_SELECTION);
		if (children.length > 0) {
			classes = new ArrayList<>();
			for (IConfigurationElement sel : children) {
				String cname = sel.getAttribute(IWorkbenchRegistryConstants.ATT_CLASS);
				String name = sel.getAttribute(IWorkbenchRegistryConstants.ATT_NAME);
				SelectionClass sclass = new SelectionClass();
				sclass.className = cname;
				sclass.nameFilter = name;
				classes.add(sclass);
			}
		}
	}

	/**
	 * Verifies if the element is an instance of a class with a given class name. If
	 * direct match fails, implementing interfaces will be tested, then recursively
	 * all superclasses and their interfaces.
	 */
	private boolean verifyClass(Object element, String className) {
		Class<?> eclass = element.getClass();
		Class<?> clazz = eclass;
		boolean match = false;
		while (clazz != null) {
			// test the class itself
			if (clazz.getName().equals(className)) {
				match = true;
				break;
			}
			// test all the interfaces it implements
			Class<?>[] interfaces = clazz.getInterfaces();
			for (Class<?> currentInterface : interfaces) {
				if (currentInterface.getName().equals(className)) {
					match = true;
					break;
				}
			}
			if (match == true) {
				break;
			}
			// get the superclass
			clazz = clazz.getSuperclass();
		}
		return match;
	}

	/**
	 * Verifies if the given element matches one of the selection requirements.
	 * Element must at least pass the type test, and optionally wildcard name match.
	 */
	private boolean verifyElement(IAdaptable element) {
		if (classes.isEmpty()) {
			return true;
		}
		for (SelectionClass sc : classes) {
			if (verifyClass(element, sc.className) == false) {
				continue;
			}
			if (sc.nameFilter == null) {
				return true;
			}
			IWorkbenchAdapter de = Adapters.adapt(element, IWorkbenchAdapter.class);
			if ((de != null) && verifyNameMatch(de.getLabel(element), sc.nameFilter)) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Compare selection count with requirements.
	 */
	private boolean verifySelectionCount(int count) {
		if (count > 0 && mode == NONE) {
			return false;
		}
		if (count == 0 && mode == ONE_OR_MORE) {
			return false;
		}
		if (count > 1 && mode == NONE_OR_ONE) {
			return false;
		}
		if (count < 2 && mode == MULTIPLE) {
			return false;
		}
		if (mode > 0 && count != mode) {
			return false;
		}
		return true;
	}
}
